/*
 * XXX ab@apache.org: This class is copied verbatim from Xalan-J 2.6.0
 * XXX distribution, org.apache.xml.utils.DOMBuilder, in order to
 * avoid dependency on Xalan.
 */

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id: DOMBuilder.java,v 1.19 2004/02/25 13:07:51 aruny Exp $
 */
package org.apache.nutch.parse.html;

import java.util.Stack;

import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.w3c.dom.CDATASection;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.ext.LexicalHandler;

/**
 * This class takes SAX events (in addition to some extra events that SAX
 * doesn't handle yet) and adds the result to a document or document fragment.
 */
public class DOMBuilder implements ContentHandler, LexicalHandler {

	/** Root document */
	public Document m_doc;

	/** Current node */
	protected Node m_currentNode = null;

	/** First node of document fragment or null if not a DocumentFragment */
	public DocumentFragment m_docFrag = null;

	/** Vector of element nodes */
	protected Stack<Element> m_elemStack = new Stack<Element>();

	/**
	 * DOMBuilder instance constructor... it will add the DOM nodes to the
	 * document fragment.
	 * 
	 * @param doc
	 *            Root document
	 * @param node
	 *            Current node
	 */
	public DOMBuilder(Document doc, Node node) {
		m_doc = doc;
		m_currentNode = node;
	}

	/**
	 * DOMBuilder instance constructor... it will add the DOM nodes to the
	 * document fragment.
	 * 
	 * @param doc
	 *            Root document
	 * @param docFrag
	 *            Document fragment
	 */
	public DOMBuilder(Document doc, DocumentFragment docFrag) {
		m_doc = doc;
		m_docFrag = docFrag;
	}

	/**
	 * DOMBuilder instance constructor... it will add the DOM nodes to the
	 * document.
	 * 
	 * @param doc
	 *            Root document
	 */
	public DOMBuilder(Document doc) {
		m_doc = doc;
	}

	/**
	 * Get the root node of the DOM being created. This is either a Document or
	 * a DocumentFragment.
	 * 
	 * @return The root document or document fragment if not null
	 */
	public Node getRootNode() {
		return (null != m_docFrag) ? (Node) m_docFrag : (Node) m_doc;
	}

	/**
	 * Get the node currently being processed.
	 * 
	 * @return the current node being processed
	 */
	public Node getCurrentNode() {
		return m_currentNode;
	}

	/**
	 * Return null since there is no Writer for this class.
	 * 
	 * @return null
	 */
	public java.io.Writer getWriter() {
		return null;
	}

	/**
	 * Append a node to the current container.
	 * 
	 * @param newNode
	 *            New node to append
	 */
	protected void append(Node newNode) throws org.xml.sax.SAXException {

		Node currentNode = m_currentNode;

		if (null != currentNode) {
			currentNode.appendChild(newNode);

			// System.out.println(newNode.getNodeName());
		} else if (null != m_docFrag) {
			m_docFrag.appendChild(newNode);
		} else {
			boolean ok = true;
			short type = newNode.getNodeType();

			if (type == Node.TEXT_NODE) {
				String data = newNode.getNodeValue();

				if ((null != data) && (data.trim().length() > 0)) {
					throw new org.xml.sax.SAXException(
							"Warning: can't output text before document element!  Ignoring...");
				}

				ok = false;
			} else if (type == Node.ELEMENT_NODE) {
				if (m_doc.getDocumentElement() != null) {
					throw new org.xml.sax.SAXException(
							"Can't have more than one root on a DOM!");
				}
			}

			if (ok)
				m_doc.appendChild(newNode);
		}
	}

	/**
	 * Receive an object for locating the origin of SAX document events.
	 * 
	 * <p>
	 * SAX parsers are strongly encouraged (though not absolutely required) to
	 * supply a locator: if it does so, it must supply the locator to the
	 * application by invoking this method before invoking any of the other
	 * methods in the ContentHandler interface.
	 * </p>
	 * 
	 * <p>
	 * The locator allows the application to determine the end position of any
	 * document-related event, even if the parser is not reporting an error.
	 * Typically, the application will use this information for reporting its
	 * own errors (such as character content that does not match an
	 * application's business rules). The information returned by the locator is
	 * probably not sufficient for use with a search engine.
	 * </p>
	 * 
	 * <p>
	 * Note that the locator will return correct information only during the
	 * invocation of the events in this interface. The application should not
	 * attempt to use it at any other time.
	 * </p>
	 * 
	 * @param locator
	 *            An object that can return the location of any SAX document
	 *            event.
	 * @see org.xml.sax.Locator
	 */
	public void setDocumentLocator(Locator locator) {

		// No action for the moment.
	}

	/**
	 * Receive notification of the beginning of a document.
	 * 
	 * <p>
	 * The SAX parser will invoke this method only once, before any other
	 * methods in this interface or in DTDHandler (except for
	 * setDocumentLocator).
	 * </p>
	 */
	public void startDocument() throws org.xml.sax.SAXException {

		// No action for the moment.
	}

	/**
	 * Receive notification of the end of a document.
	 * 
	 * <p>
	 * The SAX parser will invoke this method only once, and it will be the last
	 * method invoked during the parse. The parser shall not invoke this method
	 * until it has either abandoned parsing (because of an unrecoverable error)
	 * or reached the end of input.
	 * </p>
	 */
	public void endDocument() throws org.xml.sax.SAXException {

		// No action for the moment.
	}

	/**
	 * Receive notification of the beginning of an element.
	 * 
	 * <p>
	 * The Parser will invoke this method at the beginning of every element in
	 * the XML document; there will be a corresponding endElement() event for
	 * every startElement() event (even when the element is empty). All of the
	 * element's content will be reported, in order, before the corresponding
	 * endElement() event.
	 * </p>
	 * 
	 * <p>
	 * If the element name has a namespace prefix, the prefix will still be
	 * attached. Note that the attribute list provided will contain only
	 * attributes with explicit values (specified or defaulted): #IMPLIED
	 * attributes will be omitted.
	 * </p>
	 * 
	 * 
	 * @param ns
	 *            The namespace of the node
	 * @param localName
	 *            The local part of the qualified name
	 * @param name
	 *            The element name.
	 * @param atts
	 *            The attributes attached to the element, if any.
	 * @see #endElement
	 * @see org.xml.sax.Attributes
	 */
	public void startElement(String ns, String localName, String name,
			Attributes atts) throws org.xml.sax.SAXException {

		Element elem;

		// Note that the namespace-aware call must be used to correctly
		// construct a Level 2 DOM, even for non-namespaced nodes.
		if ((null == ns) || (ns.length() == 0))
			elem = m_doc.createElementNS(null, name);
		else
			elem = m_doc.createElementNS(ns, name);

		append(elem);

		try {
			int nAtts = atts.getLength();

			if (0 != nAtts) {
				for (int i = 0; i < nAtts; i++) {

					// System.out.println("type " + atts.getType(i) + " name " +
					// atts.getLocalName(i) );
					// First handle a possible ID attribute
					if (atts.getType(i).equalsIgnoreCase("ID"))
						setIDAttribute(atts.getValue(i), elem);

					String attrNS = atts.getURI(i);

					if ("".equals(attrNS))
						attrNS = null; // DOM represents no-namespace as null

					// System.out.println("attrNS: "+attrNS+", localName:
					// "+atts.getQName(i)
					// +", qname: "+atts.getQName(i)+", value:
					// "+atts.getValue(i));
					// Crimson won't let us set an xmlns: attribute on the DOM.
					String attrQName = atts.getQName(i);

					// In SAX, xmlns: attributes have an empty namespace, while
					// in DOM
					// they should have the xmlns namespace
					if (attrQName.startsWith("xmlns:"))
						attrNS = "http://www.w3.org/2000/xmlns/";

					// ALWAYS use the DOM Level 2 call!
					elem.setAttributeNS(attrNS, attrQName, atts.getValue(i));
				}
			}

			// append(elem);

			m_elemStack.push(elem);

			m_currentNode = elem;

			// append(elem);
		} catch (java.lang.Exception de) {
			// de.printStackTrace();
			throw new org.xml.sax.SAXException(de);
		}

	}

	/**
	 * 
	 * 
	 * 
	 * Receive notification of the end of an element.
	 * 
	 * <p>
	 * The SAX parser will invoke this method at the end of every element in the
	 * XML document; there will be a corresponding startElement() event for
	 * every endElement() event (even when the element is empty).
	 * </p>
	 * 
	 * <p>
	 * If the element name has a namespace prefix, the prefix will still be
	 * attached to the name.
	 * </p>
	 * 
	 * 
	 * @param ns
	 *            the namespace of the element
	 * @param localName
	 *            The local part of the qualified name of the element
	 * @param name
	 *            The element name
	 */
	public void endElement(String ns, String localName, String name)
			throws org.xml.sax.SAXException {
		m_elemStack.pop();
		m_currentNode = m_elemStack.isEmpty() ? null : m_elemStack.peek();
	}

	/**
	 * Set an ID string to node association in the ID table.
	 * 
	 * @param id
	 *            The ID string.
	 * @param elem
	 *            The associated ID.
	 */
	public void setIDAttribute(String id, Element elem) {

		// Do nothing. This method is meant to be overiden.
	}

	/**
	 * Receive notification of character data.
	 * 
	 * <p>
	 * The Parser will call this method to report each chunk of character data.
	 * SAX parsers may return all contiguous character data in a single chunk,
	 * or they may split it into several chunks; however, all of the characters
	 * in any single event must come from the same external entity, so that the
	 * Locator provides useful information.
	 * </p>
	 * 
	 * <p>
	 * The application must not attempt to read from the array outside of the
	 * specified range.
	 * </p>
	 * 
	 * <p>
	 * Note that some parsers will report whitespace using the
	 * ignorableWhitespace() method rather than this one (validating parsers
	 * must do so).
	 * </p>
	 * 
	 * @param ch
	 *            The characters from the XML document.
	 * @param start
	 *            The start position in the array.
	 * @param length
	 *            The number of characters to read from the array.
	 * @see #ignorableWhitespace
	 * @see org.xml.sax.Locator
	 */
	public void characters(char ch[], int start, int length)
			throws org.xml.sax.SAXException {
		if (isOutsideDocElem()
				&& XMLCharacterRecognizer.isWhiteSpace(ch, start, length))
			return; // avoid DOM006 Hierarchy request error

		if (m_inCData) {
			cdata(ch, start, length);

			return;
		}

		String s = new String(ch, start, length);
		Node childNode;
		childNode = m_currentNode != null ? m_currentNode.getLastChild() : null;
		if (childNode != null && childNode.getNodeType() == Node.TEXT_NODE) {
			((Text) childNode).appendData(s);
		} else {
			Text text = m_doc.createTextNode(s);
			append(text);
		}
	}

	/**
	 * If available, when the disable-output-escaping attribute is used, output
	 * raw text without escaping. A PI will be inserted in front of the node
	 * with the name "lotusxsl-next-is-raw" and a value of "formatter-to-dom".
	 * 
	 * @param ch
	 *            Array containing the characters
	 * @param start
	 *            Index to start of characters in the array
	 * @param length
	 *            Number of characters in the array
	 */
	public void charactersRaw(char ch[], int start, int length)
			throws org.xml.sax.SAXException {
		if (isOutsideDocElem()
				&& XMLCharacterRecognizer.isWhiteSpace(ch, start, length))
			return; // avoid DOM006 Hierarchy request error

		String s = new String(ch, start, length);

		append(m_doc.createProcessingInstruction("xslt-next-is-raw",
				"formatter-to-dom"));
		append(m_doc.createTextNode(s));
	}

	/**
	 * Report the beginning of an entity.
	 * 
	 * The start and end of the document entity are not reported. The start and
	 * end of the external DTD subset are reported using the pseudo-name
	 * "[dtd]". All other events must be properly nested within start/end entity
	 * events.
	 * 
	 * @param name
	 *            The name of the entity. If it is a parameter entity, the name
	 *            will begin with '%'.
	 * @see #endEntity
	 * @see org.xml.sax.ext.DeclHandler#internalEntityDecl
	 * @see org.xml.sax.ext.DeclHandler#externalEntityDecl
	 */
	public void startEntity(String name) throws org.xml.sax.SAXException {

		// Almost certainly the wrong behavior...
		// entityReference(name);
	}

	/**
	 * Report the end of an entity.
	 * 
	 * @param name
	 *            The name of the entity that is ending.
	 * @see #startEntity
	 */
	public void endEntity(String name) throws org.xml.sax.SAXException {
	}

	/**
	 * Receive notivication of a entityReference.
	 * 
	 * @param name
	 *            name of the entity reference
	 */
	public void entityReference(String name) throws org.xml.sax.SAXException {
		append(m_doc.createEntityReference(name));
	}

	/**
	 * Receive notification of ignorable whitespace in element content.
	 * 
	 * <p>
	 * Validating Parsers must use this method to report each chunk of ignorable
	 * whitespace (see the W3C XML 1.0 recommendation, section 2.10):
	 * non-validating parsers may also use this method if they are capable of
	 * parsing and using content models.
	 * </p>
	 * 
	 * <p>
	 * SAX parsers may return all contiguous whitespace in a single chunk, or
	 * they may split it into several chunks; however, all of the characters in
	 * any single event must come from the same external entity, so that the
	 * Locator provides useful information.
	 * </p>
	 * 
	 * <p>
	 * The application must not attempt to read from the array outside of the
	 * specified range.
	 * </p>
	 * 
	 * @param ch
	 *            The characters from the XML document.
	 * @param start
	 *            The start position in the array.
	 * @param length
	 *            The number of characters to read from the array.
	 * @see #characters
	 */
	public void ignorableWhitespace(char ch[], int start, int length)
			throws org.xml.sax.SAXException {
		if (isOutsideDocElem())
			return; // avoid DOM006 Hierarchy request error

		String s = new String(ch, start, length);

		append(m_doc.createTextNode(s));
	}

	/**
	 * Tell if the current node is outside the document element.
	 * 
	 * @return true if the current node is outside the document element.
	 */
	private boolean isOutsideDocElem() {
		return (null == m_docFrag) && m_elemStack.size() == 0
				&& (null == m_currentNode
						|| m_currentNode.getNodeType() == Node.DOCUMENT_NODE);
	}

	/**
	 * Receive notification of a processing instruction.
	 * 
	 * <p>
	 * The Parser will invoke this method once for each processing instruction
	 * found: note that processing instructions may occur before or after the
	 * main document element.
	 * </p>
	 * 
	 * <p>
	 * A SAX parser should never report an XML declaration (XML 1.0, section
	 * 2.8) or a text declaration (XML 1.0, section 4.3.1) using this method.
	 * </p>
	 * 
	 * @param target
	 *            The processing instruction target.
	 * @param data
	 *            The processing instruction data, or null if none was supplied.
	 */
	public void processingInstruction(String target, String data)
			throws org.xml.sax.SAXException {
		append(m_doc.createProcessingInstruction(target, data));
	}

	/**
	 * Report an XML comment anywhere in the document.
	 * 
	 * This callback will be used for comments inside or outside the document
	 * element, including comments in the external DTD subset (if read).
	 * 
	 * @param ch
	 *            An array holding the characters in the comment.
	 * @param start
	 *            The starting position in the array.
	 * @param length
	 *            The number of characters to use from the array.
	 */
	public void comment(char ch[], int start, int length)
			throws org.xml.sax.SAXException {
		// tagsoup sometimes submits invalid values here
		if (ch == null || start < 0 || length >= (ch.length - start)
				|| length < 0)
			return;
		append(m_doc.createComment(new String(ch, start, length)));
	}

	/** Flag indicating that we are processing a CData section */
	protected boolean m_inCData = false;

	/**
	 * Report the start of a CDATA section.
	 * 
	 * @see #endCDATA
	 */
	public void startCDATA() throws org.xml.sax.SAXException {
		m_inCData = true;
		append(m_doc.createCDATASection(""));
	}

	/**
	 * Report the end of a CDATA section.
	 * 
	 * @see #startCDATA
	 */
	public void endCDATA() throws org.xml.sax.SAXException {
		m_inCData = false;
	}

	/**
	 * Receive notification of cdata.
	 * 
	 * <p>
	 * The Parser will call this method to report each chunk of character data.
	 * SAX parsers may return all contiguous character data in a single chunk,
	 * or they may split it into several chunks; however, all of the characters
	 * in any single event must come from the same external entity, so that the
	 * Locator provides useful information.
	 * </p>
	 * 
	 * <p>
	 * The application must not attempt to read from the array outside of the
	 * specified range.
	 * </p>
	 * 
	 * <p>
	 * Note that some parsers will report whitespace using the
	 * ignorableWhitespace() method rather than this one (validating parsers
	 * must do so).
	 * </p>
	 * 
	 * @param ch
	 *            The characters from the XML document.
	 * @param start
	 *            The start position in the array.
	 * @param length
	 *            The number of characters to read from the array.
	 * @see #ignorableWhitespace
	 * @see org.xml.sax.Locator
	 */
	public void cdata(char ch[], int start, int length)
			throws org.xml.sax.SAXException {
		if (isOutsideDocElem()
				&& XMLCharacterRecognizer.isWhiteSpace(ch, start, length))
			return; // avoid DOM006 Hierarchy request error

		String s = new String(ch, start, length);

		// XXX ab@apache.org: modified from the original, to accomodate TagSoup.
		Node n = m_currentNode.getLastChild();
		if (n instanceof CDATASection)
			((CDATASection) n).appendData(s);
		else if (n instanceof Comment)
			((Comment) n).appendData(s);
	}

	/**
	 * Report the start of DTD declarations, if any.
	 * 
	 * Any declarations are assumed to be in the internal subset unless
	 * otherwise indicated.
	 * 
	 * @param name
	 *            The document type name.
	 * @param publicId
	 *            The declared public identifier for the external DTD subset, or
	 *            null if none was declared.
	 * @param systemId
	 *            The declared system identifier for the external DTD subset, or
	 *            null if none was declared.
	 * @see #endDTD
	 * @see #startEntity
	 */
	public void startDTD(String name, String publicId, String systemId)
			throws org.xml.sax.SAXException {

		// Do nothing for now.
	}

	/**
	 * Report the end of DTD declarations.
	 * 
	 * @see #startDTD
	 */
	public void endDTD() throws org.xml.sax.SAXException {

		// Do nothing for now.
	}

	/**
	 * Begin the scope of a prefix-URI Namespace mapping.
	 * 
	 * <p>
	 * The information from this event is not necessary for normal Namespace
	 * processing: the SAX XML reader will automatically replace prefixes for
	 * element and attribute names when the
	 * http://xml.org/sax/features/namespaces feature is true (the default).
	 * </p>
	 * 
	 * <p>
	 * There are cases, however, when applications need to use prefixes in
	 * character data or in attribute values, where they cannot safely be
	 * expanded automatically; the start/endPrefixMapping event supplies the
	 * information to the application to expand prefixes in those contexts
	 * itself, if necessary.
	 * </p>
	 * 
	 * <p>
	 * Note that start/endPrefixMapping events are not guaranteed to be properly
	 * nested relative to each-other: all startPrefixMapping events will occur
	 * before the corresponding startElement event, and all endPrefixMapping
	 * events will occur after the corresponding endElement event, but their
	 * order is not guaranteed.
	 * </p>
	 * 
	 * @param prefix
	 *            The Namespace prefix being declared.
	 * @param uri
	 *            The Namespace URI the prefix is mapped to.
	 * @see #endPrefixMapping
	 * @see #startElement
	 */
	public void startPrefixMapping(String prefix, String uri)
			throws org.xml.sax.SAXException {

		/*
		 * // Not sure if this is needed or wanted // Also, it fails in the
		 * stree. if((null != m_currentNode) && (m_currentNode.getNodeType() ==
		 * Node.ELEMENT_NODE)) { String qname; if(((null != prefix) &&
		 * (prefix.length() == 0)) || (null == prefix)) qname = "xmlns"; else
		 * qname = "xmlns:"+prefix;
		 * 
		 * Element elem = (Element)m_currentNode; String val =
		 * elem.getAttribute(qname); // Obsolete, should be DOM2...? if(val ==
		 * null) { elem.setAttributeNS("http://www.w3.org/XML/1998/namespace",
		 * qname, uri); } }
		 */
	}

	/**
	 * End the scope of a prefix-URI mapping.
	 * 
	 * <p>
	 * See startPrefixMapping for details. This event will always occur after
	 * the corresponding endElement event, but the order of endPrefixMapping
	 * events is not otherwise guaranteed.
	 * </p>
	 * 
	 * @param prefix
	 *            The prefix that was being mapping.
	 * @see #startPrefixMapping
	 * @see #endElement
	 */
	public void endPrefixMapping(String prefix)
			throws org.xml.sax.SAXException {
	}

	/**
	 * Receive notification of a skipped entity.
	 * 
	 * <p>
	 * The Parser will invoke this method once for each entity skipped.
	 * Non-validating processors may skip entities if they have not seen the
	 * declarations (because, for example, the entity was declared in an
	 * external DTD subset). All processors may skip external entities,
	 * depending on the values of the
	 * http://xml.org/sax/features/external-general-entities and the
	 * http://xml.org/sax/features/external-parameter-entities properties.
	 * </p>
	 * 
	 * @param name
	 *            The name of the skipped entity. If it is a parameter entity,
	 *            the name will begin with '%'.
	 */
	public void skippedEntity(String name) throws org.xml.sax.SAXException {
	}
}
